# pytombo - Python library for processing plain text + encrypted notes.
# Copyright (C) 2007  Chris Clark

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Module to handle note operations and (encrypted) file IO for Tombo notes
Tombo is a very cool notes application available from 
    http://tombo.sourceforge.jp/En/
    
"""

import sys
import os
try:
    import getpass
except ImportError:
    class getpass(object):
        def getpass(cls, prompt=None, stream=None):
            if prompt is None:
                prompt=''
            if stream is None:
                stream=sys.stdout
            prompt=prompt+ ' (warning password WILL echo): '
            stream.write(prompt)
            result = raw_input('') # or simply ignore stream??
            return result
        getpass = classmethod(getpass)

import chi_io


this_module_path = os.path.abspath(os.path.dirname(__file__))

## TODO! propagate around modules
ENCRYPTED_EXTENSION = '.chi'
TEXT_EXTENSION = '.txt'

DEBUG_CHECK_NOTE=True
#DEBUG_CHECK_NOTE=False

CR = '\r'
NL = '\n'
DOS_CRNL = '\r\n'
def dos2unix(in_str):
    """In-efficient but simple dos2unix string conversion
    convert '\x0D\x0A' --> '\x0A'
    """
    ## TODO quicker to just remove '\r and leave '\n' alone?
    return in_str.replace(DOS_CRNL, NL)

def unix2dos(in_str):
    """In-efficient but simple unix2dos string conversion
    convert '\x0A' --> '\x0D\x0A'
    WARNING! Do not run this routine on a file that contains '\r'
    """
    return in_str.replace('\x0A', '\x0D\x0A')


def getpassfunc(prompt=None):
    if prompt:
        if sys.platform == 'win32' and isinstance(prompt, unicode):
            #if windows, deal with unicode problem in console
            # TODO add Windows check here!
            prompt=repr(prompt)
        return getpass.getpass(prompt)
    else:
        return getpass.getpass()

def remove_leading_path(ignore_path, input_path):
    """returns input_path with (leading) ignore_path removed.
    If ignore_path is not at the start of the input_path, return the original
    input_path"""
    abs_ignore_path = os.path.abspath(ignore_path)
    abs_ignore_path = os.path.join(abs_ignore_path, '') ## add trailing slash
    abs_input_path = os.path.abspath(input_path)
    if abs_input_path.startswith(abs_ignore_path):
        return abs_input_path[len(abs_ignore_path):]
    else:
        return input_path


def loadplain_file(filename, length_limit=None, dos_newlines=True):
    """Given fully qualified filename, load note and return contents.
    As for all file io, io is on bytes (i.e. not Unicode chars)
    If unicode is required encode to byte stream before hand.
    Returns None on bad read, missing file, etc.
    TODO consider adding encoding option/parameter?
    
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    
    Consider using file_contents() instead.
    """
    out_text=None
    tmpfile = open(filename, 'rb')  # FIXME use "rU" Universal newline support
    if length_limit is None:
        # Default - read entire file
        out_text = tmpfile.read()
    else:
        # assume length_limit is a numeric
        out_text = tmpfile.read(length_limit)
    tmpfile.close()
    
    if dos_newlines:
        if '\r' in out_text:
            out_text = dos2unix(out_text)
    return out_text
    
def saveplain_file(filename, notetext, dos_newlines=True):
    """Given fully qualified filename, and note text, save note.
    As for all file io, io is on bytes (i.e. not Unicode chars)
    If unicode is required encode to byte stream before hand.
    If note contains Unicode strings, standard (default)
    encoding is used which could fail.
    TODO consider adding encoding option/parameter?
    
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    """
    if dos_newlines:
        if '\r' in notetext:
            notetext = dos2unix(notetext)
        notetext = unix2dos(notetext)
    tmpfile = open(filename, 'wb')
    tmpfile.write(notetext)
    tmpfile.close()
    if DEBUG_CHECK_NOTE:
        saved_text = loadplain_file(filename, dos_newlines=False)
        assert saved_text == notetext, 'saveplain_file(), saved text does not match input text'

def decrypt_file(filename, password, dos_newlines=True):
    """Given filename and password returns decrypted note.
    Decrypted note is in a byte stream.
    
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    
    Consider using file_contents() instead.
    """
    out_text = chi_io.read_encrypted_file(filename, password)
    if dos_newlines:
        if '\r' in out_text:
            out_text = dos2unix(out_text)
    return out_text

def decrypt_file_ignore_errors(filename, password):
    """Given filename and password returns decrypted note.
    Decrypted note is in a byte stream.
    Returns None on bad read, missing file, bad password etc.
    
    Consider using file_contents() instead.
    """
    out_text=None
    
    try:
        out_text = chi_io.read_encrypted_file(filename, password)
    except chi_io.BadPassword, info:
        pass
    except chi_io.UnsupportedFile, info:
        pass
    
    return out_text

def encrypt_file(filename, notetext, password, dos_newlines=True):
    """Given filename and password saves encrypted note.
    Expect notetext to be a single byte stream.
    
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    """
    if dos_newlines:
        if '\r' in notetext:
            notetext = dos2unix(notetext)
        notetext = unix2dos(notetext)
    chi_io.write_encrypted_file(filename, password, notetext)
    if DEBUG_CHECK_NOTE:
        saved_text = decrypt_file(filename, password, dos_newlines=False)
        assert saved_text == notetext, 'encrypt_file(), decrypted text does not match input text'
    

#################################################################

class gen_static_password(object):
    def __init__(self, password):
        self.user_password = password
        if isinstance(self.user_password, str):
            ## not sure if this logic belongs at this level....
            self.user_password = chi_io.CHI_cipher(self.user_password)
    def gen_func(self):
        """
        def get_pass(*args, **kwargs):
            return self.user_password 
        """
        def get_pass(*args, **kwargs):
            """Returns cached password, will reset password if requested"""
            if args or kwargs.get('reset'):
                self.user_password = None
            return self.user_password 
        return get_pass

class gen_caching_get_password(object):
    def __init__(self, dirname=None):
        """If dirname is specified, removes that part of the pathname when prompting for the password for that file"""
        self.user_password = None
        self.dirname = dirname
    def gen_func(self):
        ## TODO add (optional) timeout "forget"/wipe password from memory
        def get_pass(prompt=None, filename=None, reset=False):
            """Caching password prompt for CONSOLE.
                prompt - the prompt to print for password prompt
                reset - if set to True will forget the cached password and prompt for it
            """
            if reset:
                self.user_password = None
            if self.user_password is None:
                if prompt is None:
                    if filename is None:
                        prompt = "Password:"
                    else:
                        if self.dirname is not None:
                            filename = remove_leading_path(self.dirname, filename)
                            prompt = "Password for note %s:" % filename
                        else:
                            prompt = "Password for file %s:" % filename
                self.user_password = getpassfunc(prompt)
                if self.user_password  is None or self.user_password  == '':
                    self.user_password = None
                else:
                    ## not sure if this logic belongs at this level....
                    self.user_password = chi_io.CHI_cipher(self.user_password)
            return self.user_password 
        return get_pass

class gen_caching_get_passwordWIP(object):
    def __init__(self, dirname=None, promptfunc=None):
        """If dirname is specified, removes that part of the pathname when prompting for the password for that file
        if promptfunc is specified, promptfunc is called to prompt for password, if ommitted std getpass.getpass() is used"""
        self.user_password = None
        self.dirname = dirname
        if promptfunc is None:
            promptfunc = getpassfunc
        self.promptfunc = promptfunc
    def gen_func(self):
        ## TODO add (optional) timeout "forget"/wipe password from memory
        def get_pass(prompt_str=None, filename=None, reset=False, value=None, prompt=True):
            """Caching password prompt for CONSOLE.
                prompt_str - the prompt to print for password prompt
                filename - the filename the password is required for
                reset - if set to True will set cached password to "value" (which is set to default of None means forget the cached password and prompt for it)
                value - see reset parameter
                prompt - if not True will NOT prompt user and simply return the cached password
            """
            if reset:
                self.user_password = value
                if self.user_password is not None:
                    ## not sure if this logic belongs at this level....
                    self.user_password = chi_io.CHI_cipher(self.user_password)
            if self.user_password is None and prompt:
                if prompt_str is None:
                    if filename is None:
                        prompt_str = "Password:"
                    else:
                        if self.dirname is not None:
                            filename = remove_leading_path(self.dirname, filename)
                            prompt = "Password for note %s:" % filename
                        else:
                            prompt = "Password for file %s:" % filename
                self.user_password = self.promptfunc(prompt)
                if self.user_password  is None or self.user_password  == '':
                    self.user_password = None
                else:
                    ## not sure if this logic belongs at this level....
                    self.user_password = chi_io.CHI_cipher(self.user_password)
            return self.user_password 
        return get_pass


caching_console_password_prompt = gen_caching_get_password().gen_func()
##static_password_func = gen_static_password(some_password).gen_func()
    
def file_contents(filename, get_pass, dos_newlines=True):
    """Loads a note file, either plain text or encrypted. Returns bytes.
    
        get_pass() is a callback function that returns a password, get_pass() should return None for 'cancel'. See caching_console_password_prompt() for an example.
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    """
    lower_filename = filename.lower()
    if lower_filename.endswith('.txt'):
        return loadplain_file(filename, dos_newlines=dos_newlines)
    elif lower_filename.endswith('.chi'):
        reset_password = False
        if get_pass is None:
            raise chi_io.BadPassword('None passed in for password for file %r' % filename)
        while 1:
            if callable(get_pass):
                note_password=get_pass(filename=filename, reset=reset_password)
            else:
                # Assume password bytes passed in
                note_password=get_pass
            if note_password is None:
                raise chi_io.BadPassword('missing or bad password for %s' % filename)
            reset_password = False
            try:
                plain_text = decrypt_file(filename, note_password, dos_newlines=dos_newlines)
                break
            except chi_io.BadPassword, info:
                ## We will try the file again with a new (reset) password
                reset_password = True
        return plain_text
    else:
        raise NotSupported('Unknown file type requested for read')


def save_file_contents(notetext, orig_notetext=None, filename=None, get_pass=None, dos_newlines=True, backup=True):
    """Saves a note file, either plain text or encrypted.
    NOTE only plain text is supported at the moment
    FIXME incomplete does NOT support renaming notes, and will overwrite original name
    FIXME backup not implemented
    
    orig is will be used to helpdetect changes
    
        dos_newlines=True means use Windows/DOS newlines, emulates win32 behavior of Tombo and is the default
    """
    if filename:
        lower_filename = filename.lower()
        if lower_filename.endswith('.txt'):
            return saveplain_file(filename, notetext, dos_newlines=dos_newlines)
        elif lower_filename.endswith('.chi'):
            if get_pass is None:
                raise chi_io.BadPassword('None passed in for password for file %r' % filename)
            password = get_pass  # Assumes get_pass is the password bytes
            return encrypt_file(filename, notetext, password, dos_newlines=dos_newlines)
        else:
            raise chi_io.UnsupportedFile('Unknown file type requested for write')
    else:
        raise NotSupported()


def default_note_name(in_str=None, spaces2underscore=None):
    """Returns derived filename for a note.
    Derived name is the first line (with invalid chars removed)
    not sure if should tack on .txt (as some are encrypted and are .chi)
    optional use underscores for spaces.
    Does not return unique names, caller is responsible for creating unique
    names. E.g. in Tombo a new note with a blank name saved as "memo",
    a 2nd new note with a blank name saved as "memo(1)", then "memo(2)", etc.
    """
    if spaces2underscore is None:
        spaces2underscore = False
    else:
        spaces2underscore = True

    ## take first line from in_str, strip leading/trailing whitespace
    generated_name = in_str[:in_str.find('\n')].strip()
    
    if spaces2underscore:
        generated_name.replace(' ', '_')
    
    ## TODO, this codeblock may belong in calling routine
    if generated_name == '':
        generated_name = 'memo'
        
    ## TODO remove bad chars (for filesystem)
    return generated_name

